/*
 *  Copyright (C) 2004 Cidero, Inc.
 *
 *  Permission is hereby granted to any person obtaining a copy of 
 *  this software to use, copy, modify, merge, publish, and distribute
 *  the software for any non-commercial purpose, subject to the
 *  following conditions:
 *  
 *  The above copyright notice and this permission notice shall be included
 *  in all copies or substantial portions of the Software.
 *
 *  THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS 
 *  OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 *  FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL 
 *  THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 *  LIABILITY IN CONNECTION WITH THE SOFTWARE.
 * 
 *  File: $RCSfile: SlimAVTransport.java,v $
 *
 */

package com.cidero.bridge.slim;

import java.net.MalformedURLException;
import java.net.URL;
import java.util.Observable;
import java.util.Observer;
import java.util.logging.Logger;

import org.cybergarage.upnp.Action;
import org.cybergarage.upnp.StateVariable;
import org.cybergarage.upnp.device.InvalidDescriptionException;

import com.cidero.bridge.MediaRendererException;
import com.cidero.bridge.MediaRendererSession;
import com.cidero.upnp.AVTransport;

/**
 * Slim UPnP Bridge AVTransport Service class. 
 *
 */
public class SlimAVTransport extends AVTransport
                                implements Observer
{
  private static Logger logger = Logger.getLogger("com.cidero.bridge.slim");

  SlimMediaRenderer       mediaRenderer;
  // shorthand for mediaRenderer.getStateModel()
  SlimStateModel          stateModel;

  MediaRendererSession   currentSession;
  
  /**
   * Constructor
   */
  public SlimAVTransport( SlimMediaRenderer mediaRenderer )
    throws InvalidDescriptionException
  {
    super( mediaRenderer );
    
    logger.fine("Entered SlimAVTransport constructor");

    this.mediaRenderer = mediaRenderer;

    stateModel = mediaRenderer.getStateModel();
    stateModel.addObserver(this);
    
    logger.fine("Leaving AVTransport constructor");
  }

  /**
   *  Initialize state variables. All required state variables are set to
   *  reasonable default values in base class. Override defaults as 
   *  apppropriate here, and set any optional ones
   */
  public void initializeStateVariables()
  {
    super.initializeStateVariables();

    // Override defaults (none yet)

    // Optional state variables
    setStateVariable("CurrentTransportActions",
                     "Play,Stop,Pause,Next,Previous" );
  }
  
  /**
   * Start/Restart playback of the current session.  All renderers
   * attached to current session are started/restarted.
   *
   * @param  action  UPNP Action object
   */
  public boolean actionPlay( Action action )
  {
    String id = action.getArgumentValue("InstanceID");
    String speed = action.getArgumentValue("Speed");

    logger.fine("Play: Entered, instanceId = " + id );

    // Get currentURI from state variable. State variable gets set by 
    // SetAVTransportURI action method in base class
    StateVariable stateVar = getService().getStateVariable( "AVTransportURI" );
    String currentURI = stateVar.getValue();

    String currentURIMetaData = null;
    stateVar = getService().getStateVariable( "AVTransportURIMetaData" );
    if( stateVar != null )
      currentURIMetaData = stateVar.getValue();
    
    logger.fine("play - getting session for URI " + currentURI );

    try
    {
      //
      // Check to see if there is already an active session for the 
      // requested URI. If so, join the session. If not, create a 
      // new session.  If the session is stopped (paused), it is 
      // restarted (all listening renderers are restarted as well)
      //
      if( (currentSession = 
           MediaRendererSession.findSession( currentURI )) != null )
      {
        currentSession.addRendererBridge( mediaRenderer );  // Join
      }
      else
      {
        logger.fine("Play: No session found - creating new one" );
        currentSession = new MediaRendererSession( mediaRenderer,
                                                   currentURI,
                                                   currentURIMetaData,
                                                   null,  // userAgent
                                                   0 );   // syncWaitMs
      }

      // Start device, and session if it's not already running on 
      // behalf of another device
      logger.fine("Starting session with resourceURL: " + 
                  currentSession.getResourceURL() );

      currentSession.setPlaySpeed( action.getArgumentValue("Speed" ) );
      currentSession.start();

      return true;
    }
    catch( Exception e )
    {
      logger.warning("Exception processing play action:" + e );
      return false;
    }
  }


  public boolean actionPause( Action action )
  {
    String id = action.getArgumentValue("InstanceID");

    logger.fine("Pause: Entered, instanceID = " + id );

    //    if( currentSession != null )
    //      currentSession.pause();

    //updateStateVariable( "TransportState", STATE_PAUSED_PLAYBACK );

    return true;
  }

  public boolean actionStop( Action action )
  {
    String id = action.getArgumentValue("InstanceID");

    logger.fine("Stop: Entered, instanceId = " + id );

    // Shut down most recently queued session, if it is currently running
    if( currentSession == null )
    {
      logger.fine("Stop: No active rendering session - ignoring action" );
      return false;
    }
        
    logger.fine("Stopping session for URL: " + 
                       currentSession.getResourceURL() );

    //
    // Stop session - all renderers associated with session are stopped.
    // (TODO: current design - may want way to stop just this renderer
    //
    currentSession.stop();
    currentSession = null;
    
    return true;
  }

  public boolean actionSeek( Action action )
  {
    String id = action.getArgumentValue("InstanceID");

    logger.fine("Seek: Entered, instanceID = " + id );

    return false;
  }

  public boolean actionNext( Action action )
  {
    logger.fine("Next: Entered" );

    // Shut down most recently queued session, if it is currently running
    if( currentSession == null )
    {
      logger.fine("Stop: No active rendering session - ignoring action" );
      return true;
    }

    currentSession.next();

    return true;
  }

  public boolean actionPrevious( Action action )
  {
    logger.fine("Previous: Entered" );

    // Shut down most recently queued session, if it is currently running
    if( currentSession == null )
    {
      logger.fine("Stop: No active rendering session - ignoring action" );
      return true;
    }

    currentSession.prev();

    return true;
  }
  
  /**
   *  Get position info for current playback.
   *
   *  Superclass method does almost everything needed, with the exception
   *  of sampling the play timer and setting the RelTime state variable.
   *  That functionality added here
   */
  public boolean actionGetPositionInfo( Action action )
  {
    if( currentSession != null )
    {
      // Sample the play time & set state variable
      setStateVariable("RelativeTimePosition", currentSession.getRelTime() );
    }
    
    // Get all position info from position-related state variables
    return super.actionGetPositionInfo( action );
  }
  
  /**
   * Set the play mode of the device. Updates the play mode of the 
   * session.  
   */
  public boolean actionSetPlayMode( Action action )
  {
    String newPlayMode = action.getArgumentValue("NewPlayMode");

    if( currentSession != null )
      currentSession.setPlayMode( newPlayMode );

    return updateStateVariable( "CurrentPlayMode", newPlayMode );
  }
  

  //-------------------------------------------------------------------
  // Following methods exist as separate entities from the actionXXX() 
  // methods since the actual startup of playbacks are controlled via
  // a renderer session object (to allow for synchronized playbacks
  // using multiple devices)    
  //
  // Example of control flow for 'play' action:
  //
  //   actionPlay() -> session.start() -> SlimAVTransport.play()
  //-------------------------------------------------------------------

  /**
   *  Set the URI for subsequent Slim PLAY commands. Note that
   *  this call is invoked by the renderer session, and uses the
   *  URI of the HTTP server proxy that is built in to the renderer
   *  bridge.  The URI used contains enough info so that when the 
   *  HTTP server proxy sees it, it knows device type, etc...
   *  For example, for the Slim, the URL will be something like:
   *  
   *    http://l92.168.1.100:8081/Slim/LivingRoom
   *
   *
   *  Note that Slim doesn't like URL's longer than ~120 characters
   *  when specified via the media agent interface. (Either via the
   *  SET_SERVER_URL command or the commands below that use separate
   *  components)
   */ 
  public void setTransportURI( String uri ) throws MediaRendererException
  {
    try 
    {
      URL url = new URL( uri );
    
      String host = url.getHost();
      int port = url.getPort();
      String path = url.getPath();
    
      if( (host == null) || (port < 0) || (path == null ) )
      {
        System.out.println("Error parsing URI: " + uri );
        throw new MediaRendererException("Renderer can't play URI: " + 
                                         uri );
      }

      mediaRenderer.send( "SET SERVER_TYPE MP3/HTTP\n" );
      mediaRenderer.send( "SET SERVER_ADDR " + host + "\n" );
      mediaRenderer.send( "SET SERVER_PORT " + port + "\n" );
      mediaRenderer.send( "SET MEDIA_TITLE \"" + path + "\"\n" );

      // Note: If LOAD is issued here, music starts, so defer that
      // until the PLAY command is invoked
    }
    catch( MalformedURLException e )
    {
      throw new MediaRendererException("Renderer can't play URI:" +
                                       uri + e );
    }
  }

  /** 
   * Start playback.
   *
   * Audiotron will not provide HTTP response until it actually gets
   * enough data to fill it's input buffer. To allow this routine to
   * return immediately after issuing a play cmd (the desired behavior
   * from the caller's perspective), a separate thread is used to
   * issue the command asynchronously
   *
   * @param speed    (Ignored by this driver)
   */
  public void play( String speed ) throws MediaRendererException
  {
    mediaRenderer.send( "LOAD\n", SlimMediaRenderer.DEFAULT_TIMEOUT );
    mediaRenderer.send( "PLAY\n", SlimMediaRenderer.DEFAULT_TIMEOUT );

    // Assume success - update state variable. If play fails, state will
    // be properly reset by status monitoring logic
    updateStateVariable("TransportState", AVTransport.STATE_PLAYING );

    // Temporarily force device state model to same thing, till we 
    // figure out how to get an accurate TransportState from the device itself
    // TODO: Fix this
    stateModel.setTransportState( AVTransport.STATE_PLAYING );
  }

  public synchronized void pause() throws MediaRendererException
  {
    mediaRenderer.send( "PAUSE\n", SlimMediaRenderer.DEFAULT_TIMEOUT );

    // Success - update state variable
    updateStateVariable("TransportState", AVTransport.STATE_PAUSED_PLAYBACK );

    // Temporarily force device state model to same thing, till we 
    // figure out how to get an accurate TransportState from the device itself
    // TODO: Fix this
    stateModel.setTransportState( AVTransport.STATE_PAUSED_PLAYBACK );

  }

  public synchronized void stop() throws MediaRendererException
  {
    mediaRenderer.send( "STOP\n", SlimMediaRenderer.DEFAULT_TIMEOUT );

    // Success - update state variable
    updateStateVariable("TransportState", AVTransport.STATE_STOPPED );

    // Temporarily force device state model to same thing, till we 
    // figure out how to get an accurate TransportState from the device itself
    // TODO: Fix this
    stateModel.setTransportState( AVTransport.STATE_STOPPED );
  }

  /**
   *  RenderingControl is an observer of SlimStatus so that it can
   *  pick up state changes made using the Slim front panel or remote.
   *  If the SlimStatus info differs from the UPnP state variables,
   *  update the state variables and fire off a LastChange event
   */
  public void update( Observable model, Object arg )
  {
    SlimStateModel stateModel = (SlimStateModel)model;

    //
    // Update the UPnP state variables to match the state model
    // Right now the only state relative to the AVTransport service 
    // that is being retrived from Slim is the TransportState
    //
    logger.fine("Updating UPnP state from model - transportState: " + 
                stateModel.getTransportState() );

    updateStateVariable( "TransportState", stateModel.getTransportState() );
  }

}

